package pl.elka.pobr.ppr.logic;

import java.awt.Image;
import java.awt.Toolkit;
import java.awt.image.BufferedImage;
import java.awt.image.MemoryImageSource;
import java.awt.image.PixelGrabber;
import java.util.Arrays;

import pl.elka.pobr.ppr.logic.recognition.MomentsComputer;
import pl.elka.pobr.ppr.logic.recognition.Recognizer;
import pl.elka.pobr.ppr.logic.recognition.Segmentator;
import pl.elka.pobr.ppr.utils.BufferedImageConverter;
import pl.elka.pobr.ppr.utils.ColorTransforms;
import pl.elka.pobr.ppr.utils.Log;

public class ImageProcessor {

    private static final String LOG_TAG = "ImageProcessor";
    private static final int MIN_SEGMENT_SIZE = 20;
    private ImageHolder mImage;
    private int mImgWidth;
    private int mImgHeight;
    private ProgressCallback mCallback;
    private Pixel[][] mPixels;
    private Pixel[][] mPixelsResult;
    private int[] mRawPixels;
    private boolean[][] mBinarized;

    public ImageProcessor(ImageHolder image, ProgressCallback callback) {
        mImage = image;
        mCallback = callback;
        mImgWidth = image.getImageSize().width;
        mImgHeight = image.getImageSize().height;
    }
    boolean a = false;

    public void processImage() {
        Log.d(LOG_TAG, ">> processImage()");
        if (mRawPixels == null || mPixels == null || mPixelsResult == null) {
            if (!preparePixelTables()) {
                return;
            }
        }
        sharpenImage(false);
        mBinarized = binarizeBlackWhite(mPixels);
        // mBinarized = binarize(mPixels);
        // mBinarized = rankFilter(mBinarized, 6);
        Segmentator seg = new Segmentator(mBinarized);
        seg.segmentate();
        Log.d(LOG_TAG, "before remove seg size=" + seg.getSegments().size());
        seg.removeSmallSegments(MIN_SEGMENT_SIZE, mBinarized);
        Log.d(LOG_TAG, "after remove seg size=" + seg.getSegments().size());
        MomentsComputer mc = new MomentsComputer(seg.getSegments(), mBinarized);
        mc.computeMoments();
        Recognizer reco = new Recognizer(seg.getSegments(), mBinarized, mPixels);
        reco.recognize();
        mCallback.calculationFinished(createResultImage(reco.getRealBitmap()));
        mCallback.calculationFinished(createResultImage(reco.getOriginalBitmap()), true);
        Log.d(LOG_TAG, "<< processImage()");
    }

    private boolean[][] binarizeBlackWhite(Pixel[][] src) {
        final int tresholdVal = 172;
        boolean[][] res = new boolean[mImgWidth][mImgHeight];
        for (int y = 0; y < mImgHeight; ++y) {
            for (int x = 0; x < mImgWidth; ++x) {
                int r = src[x][y].r; // red pixel with saturation rval
                int g = src[x][y].g; // green pixel with saturation gval
                int b = src[x][y].b; // blue pixel with saturation bval
                double[] hsv = ColorTransforms.rgb2hsv(r, g, b);
                if (r > tresholdVal && g > tresholdVal && b > tresholdVal) {
                    res[x][y] = true;
                } else if ((hsv[0] < 20 || hsv[0] > 330) && hsv[2] > 34) {
                    res[x][y] = true;
                } else {
                    res[x][y] = false;
                }
            }
        }
        return res;
    }

    private boolean[][] binarize(Pixel[][] src) {
        boolean[][] res = new boolean[mImgWidth][mImgHeight];
        for (int y = 0; y < mImgHeight; ++y) {
            for (int x = 0; x < mImgWidth; ++x) {
                int r = src[x][y].r; // red pixel with saturation rval
                int g = src[x][y].g; // green pixel with saturation gval
                int b = src[x][y].b; // blue pixel with saturation bval
                double grey = 0.212671 * r + 0.715160 * g + 0.072169 * b;
                if (grey > 170) {
                    res[x][y] = true;
                } else {
                    res[x][y] = false;
                }
            }
        }
        return res;
    }

    /**
     * @param src
     * @param val
     *            must be between 1-9 (where 1 is min filter, 9 is max filter)
     * @return
     */
    private boolean[][] rankFilter(boolean[][] org, int val) {
        int w = org.length;
        int h = org[0].length;
        boolean[] pixelWindow = new boolean[9];
        boolean[][] res = new boolean[w][h];
        for (int y = 0; y < h; ++y) {
            for (int x = 0; x < w; ++x) {
                if (y == 0 || y == h - 1 || x == 0 || x == w - 1) {
                    res[x][y] = org[x][y];
                    continue; // don't mask edges
                }
                pixelWindow[0] = org[x - 1][y - 1];
                pixelWindow[1] = org[x - 1][y];
                pixelWindow[2] = org[x - 1][y + 1];
                pixelWindow[3] = org[x][y - 1];
                pixelWindow[4] = org[x][y];
                pixelWindow[5] = org[x][y + 1];
                pixelWindow[6] = org[x + 1][y - 1];
                pixelWindow[7] = org[x + 1][y];
                pixelWindow[8] = org[x + 1][y + 1];
                int negCount = 0;
                for (int i = 0; i < pixelWindow.length; ++i) {
                    if (!pixelWindow[i]) {
                        ++negCount;
                    }
                }
                if (val > negCount) {
                    res[x][y] = true;
                } else {
                    res[x][y] = false;
                }
            }
        }
        return res;
    }

    public void blurImage() {
        if (mRawPixels == null || mPixels == null || mPixelsResult == null) {
            if (!preparePixelTables()) {
                return;
            }
        }
        mPixelsResult = MaskEngine.putMask(mPixels, MaskEngine.BLUR_MASK, true);
        // overwrite old pixels
        mPixels = Arrays.copyOf(mPixelsResult, mPixelsResult.length);
        mCallback.calculationFinished(createResultImage(mPixelsResult));
    }

    public void sharpenImage(boolean returnResult) {
        if (mRawPixels == null || mPixels == null || mPixelsResult == null) {
            if (!preparePixelTables()) {
                return;
            }
        }
        int[] res = UnsharpMaskFilter.filter(mPixels, mPixelsResult);
        mPixels = mPixelsResult = Pixel.create(res, mImgWidth, mImgHeight);
        if (returnResult) {
            mCallback.calculationFinished(createResultImage(mPixels));
        }
    }

    private BufferedImage createResultImage(Pixel[][] resultPixels) {
        int[] oneDimTable = new int[mImgWidth * mImgHeight];
        for (int i = 0; i < mImgHeight; ++i) {
            for (int j = 0; j < mImgWidth; ++j) {
                oneDimTable[j + i * mImgWidth] = resultPixels[j][i].toInt();
            }
        }
        Image img = Toolkit
                .getDefaultToolkit()
                .createImage(new MemoryImageSource(mImgWidth, mImgHeight, oneDimTable, 0, mImgWidth));
        return BufferedImageConverter.createBufferedImage(img);
    }

    private BufferedImage createResultImage(boolean[][] resultPixels) {
        int[] oneDimTable = new int[mImgWidth * mImgHeight];
        for (int i = 0; i < mImgHeight; ++i) {
            for (int j = 0; j < mImgWidth; ++j) {
                oneDimTable[j + i * mImgWidth] = resultPixels[j][i] == true ? 0xFFFFFFFF : 0xFF000000;
            }
        }
        Image img = Toolkit
                .getDefaultToolkit()
                .createImage(new MemoryImageSource(mImgWidth, mImgHeight, oneDimTable, 0, mImgWidth));
        return BufferedImageConverter.createBufferedImage(img);
    }

    private boolean preparePixelTables() {
        // grab pixels
        try {
            mRawPixels = grabPixels(mImage.getImage());
            mPixels = new Pixel[mImgWidth][mImgHeight];
            mPixelsResult = new Pixel[mImgWidth][mImgHeight];
            for (int i = 0; i < mImgHeight; ++i) {
                for (int j = 0; j < mImgWidth; ++j) {
                    mPixels[j][i] = new Pixel(mRawPixels[j + i * mImgWidth]);
                }
            }
        } catch (Exception e) {
            e.printStackTrace();
            mCallback.calculationError(e.getLocalizedMessage());
            return false;
        }
        return true;
    }

    private int[] grabPixels(BufferedImage source) throws Exception {
        PixelGrabber grabber = new PixelGrabber((Image) source, 0, 0, source.getWidth(), source.getHeight(), false);
        source.flush();
        if (!grabber.grabPixels()) {
            throw new Exception("Unable to grab pixels");
        }
        int[] pixels;
        pixels = (int[]) grabber.getPixels();
        grabber.abortGrabbing();
        grabber = null;
        return pixels;
    }

    public int[][] getHistogramValues() {
        if (mRawPixels == null || mPixels == null || mPixelsResult == null) {
            if (!preparePixelTables()) {
                return null;
            }
        }
        final Pixel[][] org = mPixels;
        int hist[][] = new int[3][256];
        for (int y = 0; y < mImgHeight; ++y) {
            for (int x = 0; x < mImgWidth; ++x) {
                int rval = org[x][y].r; // red pixel with saturation rval
                ++hist[0][rval]; // increase this saturation value red pixels
                int gval = org[x][y].g; // green pixel with saturation gval
                ++hist[1][gval]; // increase this saturation value green pixels
                int bval = org[x][y].b; // blue pixel with saturation bval
                ++hist[2][bval]; // increase this saturation value blue pixels
            }
        }
        return hist;
    }

    public int[][] histogramEqualization() {
        if (mRawPixels == null || mPixels == null || mPixelsResult == null) {
            if (!preparePixelTables()) {
                return null;
            }
        }
        int hist[][] = getHistogramValues();
        int size = mImgWidth * mImgHeight;
        int[][] nh = new int[3][256];
        double[][] distrib = new double[3][256];
        for (int k = 0; k < hist.length; ++k) {
            for (int i = 0; i < 256; ++i) {
                distrib[k][i] = sumValues(hist[k], i);
                Log.d(LOG_TAG, "hist[" + k + "][" + i + "]=" + hist[k][i] + ", distrib[" + k + "][" + i + "]=" + distrib[k][i]);
            }
            int n = 0;
            while (distrib[k][n] <= 0) {
                ++n;
            }
            double minD = distrib[k][n];
            for (int i = 0; i < 256; ++i) {
                nh[k][i] = (int) Math.floor((distrib[k][i] - minD) / (size - minD) * 255);
                Log.d(LOG_TAG, "new hist[" + k + "][" + i + "]=" + nh[k][i]);
            }
        }
        for (int y = 0; y < mImgHeight; ++y) {
            for (int x = 0; x < mImgWidth; ++x) {
                mPixelsResult[x][y] = new Pixel(nh[0][mPixels[x][y].r], nh[1][mPixels[x][y].g], nh[2][mPixels[x][y].b]);
            }
        }
        mPixels = Arrays.copyOf(mPixelsResult, mPixelsResult.length);
        hist = getHistogramValues();
        mCallback.calculationFinished(createResultImage(mPixelsResult));
        return hist;
    }

    private int sumValues(int[] tab, int index) {
        int sum = 0;
        for (int i = 0; i < tab.length && i <= index; ++i) {
            sum += tab[i];
        }
        return sum;
    }
}
